package scenery;

import com.crunch.graphics.gpu.*;
import com.crunch.math.Matrix33af;
import com.crunch.math.Matrix44f;
import com.crunch.math.Vector2f;
import com.crunch.math.Vector4f;
import resources.ResourceManager;
import util.Camera;

import javax.media.opengl.GL;
import javax.media.opengl.GL3;
import java.awt.*;

/**
 * Renders scenery.
 */
public class SceneryRenderer {
	public SceneryRenderer(ResourceManager resourceManager) {
		backgroundProgram = resourceManager.getProgram("background");
		backgroundMvpMatrixIndex = backgroundProgram.getUniformIndex("mvpMatrix");
		backgroundTexIndex = backgroundProgram.getUniformIndex("tex");
		backgroundColorIndex = backgroundProgram.getUniformIndex("color");

		tileLayerProgram = resourceManager.getProgram("tileLayer");
		tileLayerPositionIndex = tileLayerProgram.getAttributeIndex("position");
		tileLayerImagePositionIndex = tileLayerProgram.getAttributeIndex("imagePosition");
		tileLayerSizeIndex = tileLayerProgram.getAttributeIndex("size");
		tileLayerAnimationIndex = tileLayerProgram.getAttributeIndex("animation");
		tileLayerFrameOffsetIndex = tileLayerProgram.getAttributeIndex("frameOffset");
		tileLayerMvpMatrixIndex = tileLayerProgram.getUniformIndex("mvpMatrix");
		tileLayerTexIndex = tileLayerProgram.getUniformIndex("tex");
		tileLayerTimeIndex = tileLayerProgram.getUniformIndex("time");
		tileLayerColorIndex = tileLayerProgram.getUniformIndex("color");

		spriteProgram = resourceManager.getProgram("sprite");
		spriteMvpMatrixIndex = spriteProgram.getUniformIndex("mvpMatrix");
		spriteXyxyIndex = spriteProgram.getUniformIndex("xyxy");
		spriteTxyxyIndex = spriteProgram.getUniformIndex("txyxy");
		spriteTexIndex = spriteProgram.getUniformIndex("tex");
		spriteColorIndex = spriteProgram.getUniformIndex("color");

		textProgram = resourceManager.getProgram("text");
		textPositionIndex = textProgram.getAttributeIndex("position");
		textUvIndex = textProgram.getAttributeIndex("uv");
		textMvpMatrixIndex = textProgram.getUniformIndex("mvpMatrix");
		textTexIndex = textProgram.getUniformIndex("tex");
		textColorIndex = textProgram.getUniformIndex("color");

		rectProgram = resourceManager.getProgram("rect");
		rectMvpMatrixIndex = rectProgram.getUniformIndex("mvpMatrix");
		rectXyxyIndex = rectProgram.getUniformIndex("xyxy");
		rectColorIndex = rectProgram.getUniformIndex("color");
	}

	public void drawBackgroundLayer(GpuState gpuState, Camera camera, BackgroundLayerInstance backgroundLayer) {
		if (backgroundLayer.getTexture() == null) {
			return;
		}

		// attribute-less rendering
		gpuState.unbindBuffer(GL.GL_ARRAY_BUFFER);
		backgroundProgram.bind();

		backgroundProgram.setUniform(backgroundColorIndex, GL3.GL_FLOAT_VEC4, backgroundLayer.getBackgroundLayer().getColor
				().getArray());
		backgroundProgram.setUniform(backgroundTexIndex, GL3.GL_SAMPLER_2D, new int[] { 0 });
		backgroundLayer.getTexture().bind(0);

		// determine offset using parallax;
		// 0 = no parallax
		// 0-1 = "background" parallax
		// 1 = fixed with camera
		// < 0 = "foreground" parallax
		// store offset in modelMatrix
		int cornerX = (int) camera.getX() - camera.getViewportWidth()/2;
		int cornerY = (int) camera.getY() - camera.getViewportHeight()/2;
		Matrix33af modelMatrix = new Matrix33af();
		modelMatrix.translateAssign(new Vector2f(
				Math.round((float) cornerX * backgroundLayer.getBackgroundLayer().getParallaxX()) + (float) backgroundLayer.getBackgroundLayer().getOffsetX(),
				Math.round((float) cornerY * backgroundLayer.getBackgroundLayer().getParallaxY()) + (float) backgroundLayer.getBackgroundLayer().getOffsetY()));
		// projMat * viewMat * modelMat
		backgroundProgram.setUniform(backgroundMvpMatrixIndex, GL3.GL_FLOAT_MAT4,
				camera.getViewProjectionMatrix().multiply(modelMatrix.getGraphicsMatrix()).getArray());

		// draw single point - geometry shader expands it into a fullscreen quad
		GpuDrawCall.DrawCall dc = gpuState.createDrawCall();
		dc.setParameters(GL.GL_POINTS, 0, 1);
		dc.draw();
	}

	public void drawTileLayer(GpuState gpuState, Camera camera, TileLayerInstance tileLayer, int time) {
		if (tileLayer.getTexture() == null) {
			return;
		}

		tileLayerProgram.bind();

		tileLayerProgram.setUniform(tileLayerColorIndex, GL3.GL_FLOAT_VEC4, tileLayer.getTileLayer().getColor().getArray());
		tileLayerProgram.setUniform(tileLayerTexIndex, GL3.GL_SAMPLER_2D, new int[] { 0 });
		tileLayerProgram.setUniform(tileLayerTimeIndex, GL3.GL_INT, new int[] { time });
		tileLayer.getTexture().bind(0);

		// determine offset using parallax;
		// 0 = no parallax
		// 0-1 = "background" parallax
		// 1 = fixed with camera
		// < 0 = "foreground" parallax
		// store offset in modelMatrix
		int cornerX = (int) camera.getX() - camera.getViewportWidth()/2;
		int cornerY = (int) camera.getY() - camera.getViewportHeight()/2;
		int offsetX = Math.round((float) cornerX * tileLayer.getTileLayer().getParallaxX());
		int offsetY = Math.round((float) cornerY * tileLayer.getTileLayer().getParallaxY());
		Matrix33af modelMatrix = new Matrix33af();
		modelMatrix.translateAssign(new Vector2f((float) offsetX, (float) offsetY));
		// projMat * viewMat * modelMat
		tileLayerProgram.setUniform(tileLayerMvpMatrixIndex, GL3.GL_FLOAT_MAT4,
				camera.getViewProjectionMatrix().multiply(modelMatrix.getGraphicsMatrix()).getArray());

		Vector2f[] bounds = camera.getViewBounds();
		int xMin = (int) Math.floor(bounds[0].x());
		int yMin = (int) Math.floor(bounds[0].y());
		int xMax = (int) Math.ceil(bounds[1].x());
		int yMax = (int) Math.ceil(bounds[1].y());

		Point regionMin = tileLayer.getRegionForPoint(xMin - offsetX, yMin - offsetY);
		Point regionMax = tileLayer.getRegionForPoint(xMax - offsetX - 1, yMax - offsetY - 1);
		Point r = new Point();
		for (r.y = regionMin.y; r.y <= regionMax.y; ++r.y) {
			for (r.x = regionMin.x; r.x <= regionMax.x; ++r.x) {
				GpuBuffer regionBuffer = tileLayer.getRegionBuffer(r);
				if (regionBuffer == null) {
					continue;
				}
				regionBuffer.bind();

				tileLayerProgram.setAttribute(tileLayerPositionIndex, 2, GL3.GL_INT, false, true,
						TileLayerInstance.VERTEX_SIZE, TileLayerInstance.POSITION_OFFSET);
				tileLayerProgram.setAttribute(tileLayerImagePositionIndex, 2, GL3.GL_SHORT, false, true,
						TileLayerInstance.VERTEX_SIZE, TileLayerInstance.IMAGE_POSITION_OFFSET);
				tileLayerProgram.setAttribute(tileLayerSizeIndex, 2, GL3.GL_SHORT, false, true,
						TileLayerInstance.VERTEX_SIZE, TileLayerInstance.SIZE_OFFSET);
				tileLayerProgram.setAttribute(tileLayerAnimationIndex, 3, GL3.GL_SHORT, false, true,
						TileLayerInstance.VERTEX_SIZE, TileLayerInstance.ANIMATION_OFFSET);
				tileLayerProgram.setAttribute(tileLayerFrameOffsetIndex, 2, GL3.GL_SHORT, false, true,
						TileLayerInstance.VERTEX_SIZE, TileLayerInstance.FRAME_OFFSET_OFFSET);

				// draw single point - geometry shader expands it into a fullscreen quad
				GpuDrawCall.DrawCall dc = gpuState.createDrawCall();
				dc.setParameters(GL.GL_POINTS, 0, (int) regionBuffer.getSize() / TileLayerInstance.VERTEX_SIZE);
				dc.draw();

				regionBuffer.bind();
			}
		}
	}

	public void drawSprite(GpuState gpuState, Matrix44f mvpMatrix, GpuTexture texture, Vector4f color,
	                       Vector2f texMin, Vector2f texMax, Vector2f texOrigin) {
		// attribute-less rendering
		gpuState.unbindBuffer(GL.GL_ARRAY_BUFFER);
		spriteProgram.bind();

		float invTexWidth = 1.0f / texture.getWidth();
		float invTexHeight = 1.0f / texture.getHeight();
		spriteProgram.setUniform(spriteMvpMatrixIndex, GL3.GL_FLOAT_MAT4, mvpMatrix.getArray());
		spriteProgram.setUniform(spriteXyxyIndex, GL3.GL_FLOAT_VEC4,
				new Vector4f(texMin.x()- texOrigin.x(), texMin.y()-texOrigin.y(),
						texMax.x()-texOrigin.x(), texMax.y()-texOrigin.y()).getArray());
		spriteProgram.setUniform(spriteTxyxyIndex, GL3.GL_FLOAT_VEC4,
				new Vector4f(texMin.x()*invTexWidth, texMin.y()*invTexHeight,
						texMax.x()*invTexWidth, texMax.y()*invTexHeight).getArray());
		spriteProgram.setUniform(spriteColorIndex, GL3.GL_FLOAT_VEC4, color.getArray());
		spriteProgram.setUniform(spriteTexIndex, GL3.GL_SAMPLER_2D, new int[] { 0 });
		texture.bind(0);

		// draw single point - geometry shader expands it into a fullscreen quad
		GpuDrawCall.DrawCall dc = gpuState.createDrawCall();
		dc.setParameters(GL.GL_POINTS, 0, 1);
		dc.draw();
	}

	public void drawText(GpuState gpuState, Matrix44f mvpMatrix, Text text, Vector4f color) {
		text.getVertexBuffer().bind();
		textProgram.bind();

		textProgram.setUniform(textMvpMatrixIndex, GL3.GL_FLOAT_MAT4, mvpMatrix.getArray());
		textProgram.setUniform(textColorIndex, GL3.GL_FLOAT_VEC4, color.getArray());
		textProgram.setUniform(textTexIndex, GL3.GL_SAMPLER_2D, new int[] { 0 });
		text.getFont().bind(0);

		textProgram.setAttribute(textPositionIndex, 2, GL.GL_FLOAT, false, false,
				Text.VERTEX_SIZE, Text.POSITION_OFFSET);
		textProgram.setAttribute(textUvIndex, 2, GL.GL_FLOAT, false, false,
				Text.VERTEX_SIZE, Text.UV_OFFSET);

		// draw single point - geometry shader expands it into a fullscreen quad
		GpuDrawCall.DrawCall dc = gpuState.createDrawCall();
		dc.setParameters(GL.GL_TRIANGLES, 0, text.getVertexCount());
		dc.draw();
	}

	public void drawFilledRect(GpuState gpuState, Matrix44f mvpMatrix, Vector2f boundMin, Vector2f boundMax, Vector4f color) {
		// attribute-less rendering
		gpuState.unbindBuffer(GL.GL_ARRAY_BUFFER);
		rectProgram.bind();

		rectProgram.setUniform(rectMvpMatrixIndex, GL3.GL_FLOAT_MAT4, mvpMatrix.getArray());
		rectProgram.setUniform(rectXyxyIndex, GL3.GL_FLOAT_VEC4,
				new Vector4f(boundMin.x(), boundMin.y(), boundMax.x(), boundMax.y()).getArray());
		rectProgram.setUniform(rectColorIndex, GL3.GL_FLOAT_VEC4, color.getArray());

		// draw single point - geometry shader expands it into a fullscreen quad
		GpuDrawCall.DrawCall dc = gpuState.createDrawCall();
		dc.setParameters(GL.GL_POINTS, 0, 1);
		dc.draw();
	}

	public void drawRect(GpuState gpuState, Matrix44f mvpMatrix, Vector2f boundMin, Vector2f boundMax, Vector4f color, float thickness) {
		if (boundMax.x() - boundMin.x() < 2.0f * thickness || boundMax.y() - boundMin.y() < 2.0f * thickness) {
			drawRect(gpuState, mvpMatrix, boundMin, boundMax, color, thickness);
			return;
		}

		// attribute-less rendering
		gpuState.unbindBuffer(GL.GL_ARRAY_BUFFER);
		rectProgram.bind();

		rectProgram.setUniform(rectMvpMatrixIndex, GL3.GL_FLOAT_MAT4, mvpMatrix.getArray());
		rectProgram.setUniform(rectColorIndex, GL3.GL_FLOAT_VEC4, color.getArray());

		// draw 4 rects on boundary
		GpuDrawCall.DrawCall dc = gpuState.createDrawCall();
		dc.setParameters(GL.GL_POINTS, 0, 1);

		rectProgram.setUniform(rectXyxyIndex, GL3.GL_FLOAT_VEC4,
				new Vector4f(boundMin.x(), boundMin.y(), boundMin.x()+thickness, boundMax.y()).getArray());
		dc.draw();

		rectProgram.setUniform(rectXyxyIndex, GL3.GL_FLOAT_VEC4,
				new Vector4f(boundMax.x()-thickness, boundMin.y(), boundMax.x(), boundMax.y()).getArray());
		dc.draw();

		rectProgram.setUniform(rectXyxyIndex, GL3.GL_FLOAT_VEC4,
				new Vector4f(boundMin.x()+thickness, boundMin.y(), boundMax.x()-thickness, boundMin.y()+thickness).getArray());
		dc.draw();

		rectProgram.setUniform(rectXyxyIndex, GL3.GL_FLOAT_VEC4,
				new Vector4f(boundMin.x()+thickness, boundMax.y()-thickness, boundMax.x()-thickness, boundMax.y()).getArray());
		dc.draw();
	}

	private GpuProgram backgroundProgram;
	private int backgroundMvpMatrixIndex;
	private int backgroundTexIndex;
	private int backgroundColorIndex;

	private GpuProgram tileLayerProgram;
	private int tileLayerPositionIndex;
	private int tileLayerImagePositionIndex;
	private int tileLayerSizeIndex;
	private int tileLayerAnimationIndex;
	private int tileLayerFrameOffsetIndex;
	private int tileLayerMvpMatrixIndex;
	private int tileLayerTexIndex;
	private int tileLayerTimeIndex;
	private int tileLayerColorIndex;

	private GpuProgram spriteProgram;
	private int spriteMvpMatrixIndex;
	private int spriteXyxyIndex;
	private int spriteTxyxyIndex;
	private int spriteTexIndex;
	private int spriteColorIndex;

	private GpuProgram textProgram;
	private int textPositionIndex;
	private int textUvIndex;
	private int textMvpMatrixIndex;
	private int textTexIndex;
	private int textColorIndex;

	private GpuProgram rectProgram;
	private int rectMvpMatrixIndex;
	private int rectXyxyIndex;
	private int rectColorIndex;
}
